package org.elasticsearch;

import com.obs.services.*;
import com.obs.services.internal.security.BasicSecurityKey;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.cluster.metadata.RepositoryMetaData;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.settings.Settings;

import java.io.Closeable;
import java.util.Map;

import static java.util.Collections.emptyMap;

/**
 * ObsService
 */
public class ObsService implements Closeable {
    private static final Logger logger = LogManager.getLogger(ObsService.class);

    private volatile Map<ObsClientSettings, ObsReference> clientsCache = emptyMap();

    /**
     * Client settings calculated from static configuration and settings in the keystore.
     */
    private volatile Map<String, ObsClientSettings> staticClientSettings = MapBuilder.<String, ObsClientSettings>newMapBuilder().put("default", ObsClientSettings.getClientSettings(Settings.EMPTY, "default")).immutableMap();

    /**
     * Client settings derived from those in {@link #} by combining them with settings
     * in the {@link RepositoryMetaData}.
     */
    private volatile Map<Settings, ObsClientSettings> derivedClientSettings = emptyMap();

    /**
     * Attempts to retrieve a client by its repository metadata and settings from the cache.
     * If the client does not exist it will be created.
     */
    /**
     * Refreshes the settings for the Huawei OBS clients and clears the cache of
     * existing clients. New clients will be build using these new settings. Old
     * clients are usable until released. On release they will be destroyed instead
     * of being returned to the cache.
     */
    public synchronized void refreshAndClearCache(Map<String, ObsClientSettings> clientsSettings) {
        // shutdown all unused clients
        // others will shutdown on their respective release
        releaseCachedClients();
        this.staticClientSettings = MapBuilder.newMapBuilder(clientsSettings).immutableMap();
        derivedClientSettings = emptyMap();
        assert this.staticClientSettings.containsKey("default") : "always at least have 'default'";
        // clients are built lazily by {@link client}
    }

    public ObsReference client(RepositoryMetaData metadata) {
        final ObsClientSettings clientSettings = settings(metadata);
        {
            final ObsReference clientReference = clientsCache.get(clientSettings);
            if (clientReference != null && clientReference.tryIncRef()) {
                return clientReference;
            }
        }
        synchronized (this) {
            final ObsReference existing = clientsCache.get(clientSettings);
            if (existing != null && existing.tryIncRef()) {
                return existing;
            }
            final ObsReference clientReference = new ObsReference(buildClient(clientSettings));
            clientReference.incRef();
            clientsCache = MapBuilder.newMapBuilder(clientsCache).put(clientSettings, clientReference).immutableMap();
            return clientReference;
        }
    }

    ObsClientSettings settings(RepositoryMetaData repositoryMetadata) {
        final Settings settings = repositoryMetadata.settings();
        {
            final ObsClientSettings existing = derivedClientSettings.get(settings);
            if (existing != null) {
                return existing;
            }
        }
        final String clientName = ObsRepository.CLIENT_NAME.get(settings);
        final ObsClientSettings staticSettings = staticClientSettings.get(clientName);
        if (staticSettings != null) {
            synchronized (this) {
                final ObsClientSettings existing = derivedClientSettings.get(settings);
                if (existing != null) {
                    return existing;
                }
                final ObsClientSettings newSettings = staticSettings.getClientSettings(settings, clientName);
                derivedClientSettings = MapBuilder.newMapBuilder(derivedClientSettings).put(settings, newSettings).immutableMap();
                return newSettings;
            }
        }
        throw new IllegalArgumentException("Unknown OBS client name [" + clientName + "]. Existing client configs: "
                + Strings.collectionToDelimitedString(staticClientSettings.keySet(), ","));
    }

    ObsClient buildClient(final ObsClientSettings clientSettings) {

        final BasicSecurityKey credentials = clientSettings.basicSecurityKey;

        if (credentials == null) {
            return new ObsClient(new EcsObsCredentialsProvider(), buildConfiguration(clientSettings));
        } else {
            return new ObsClient(new BasicObsCredentialsProvider(credentials), buildConfiguration(clientSettings));
        }
    }

    static ObsConfiguration buildConfiguration(ObsClientSettings clientSettings) {
        final ObsConfiguration clientConfiguration = new ObsConfiguration();

        if (Strings.hasText(clientSettings.proxyHost)) {
            clientConfiguration.setHttpProxy(clientSettings.proxyHost, clientSettings.proxyPort, clientSettings.proxyUsername, clientSettings.proxyPassword);
        }

        if (Strings.hasLength(clientSettings.endpoint)) {
            clientConfiguration.setEndPoint(clientSettings.endpoint);
        }
        if (Strings.hasLength(clientSettings.region)) {
            clientConfiguration.setEndPoint(clientSettings.region);
        }

        return clientConfiguration;
    }

    private synchronized void releaseCachedClients() {
        for (final ObsReference clientReference : clientsCache.values()) {
            clientReference.decRef();
        }
        // clear previously cached clients, they will be build lazily
        clientsCache = emptyMap();
        derivedClientSettings = emptyMap();
    }

    @Override
    public void close() {
        releaseCachedClients();
    }
}
